﻿using System;
using System.Collections;
using System.Text;
using System.Xml;

namespace TemplateFoundation.Handlers.JavaScriptTranslate
{
    /// <summary>
    /// Based on code from http://www.phdcc.com/xml2json.htm
    /// </summary>
    internal class XmlToJsonConverter
    {
        /// <summary>
        /// Returns a JSON string based on the specified XML document
        /// </summary>
        /// <param name="xmlDoc"></param>
        /// <returns></returns>
        public static string Convert(XmlDocument xmlDoc)
        {
            var sb = new StringBuilder();
            
            sb.Append("{ ");
            
            XmlToJSONnode(sb, xmlDoc.DocumentElement, true);
            
            sb.Append("}");
            
            return sb.ToString();
        }

        #region Helpers

        //  XmlToJSONnode: Output an XmlElement, possibly as part of a higher array
        private static void XmlToJSONnode(StringBuilder sbJSON, XmlElement node, bool showNodeName)
        {
            bool childAdded = false;

            if (showNodeName)
            {
                sbJSON.Append("\"" + SafeJSON(node.Name) + "\": ");
            }

            sbJSON.Append("{");
            
            // Build a sorted list of key-value pairs where key is case-sensitive nodeName value is an ArrayList of string or XmlElement so that we know whether the nodeName is an array or not.
            var childNodeNames = new SortedList();

            //  Add in all node attributes
            if (node.Attributes != null)
            {
                foreach (XmlAttribute attr in node.Attributes)
                {
                    StoreChildNode(childNodeNames, attr.Name, attr.InnerText);
                }
            }

            //  Add in all nodes
            foreach (XmlNode cnode in node.ChildNodes)
            {
                childAdded = true;

                if (cnode is XmlText)
                {
                    StoreChildNode(childNodeNames, "value", cnode.InnerText);
                }
                else if (cnode is XmlElement)
                {
                    StoreChildNode(childNodeNames, cnode.Name, cnode);
                }
            }

            // Now output all stored info
            foreach (string childname in childNodeNames.Keys)
            {
                childAdded = true;

                var alChild = (ArrayList)childNodeNames[childname];

                if (alChild.Count == 1 && (alChild[0] is string))
                {
                    OutputNode(childname, alChild[0], sbJSON, true);
                }
                else
                {
                    sbJSON.Append(" \"" + SafeJSON(childname) + "\": ");

                    foreach (object Child in alChild)
                    {
                        OutputNode(childname, Child, sbJSON, false);
                    }

                    sbJSON.Remove(sbJSON.Length - 2, 2);

                    sbJSON.Append(" , ");
                }
            }

            sbJSON.Remove(sbJSON.Length - 2, 2);

            sbJSON.Append(childAdded ? " }" : " null");
        }


        //  StoreChildNode: Store data associated with each nodeName so that we know whether the nodeName is an array or not
        private static void StoreChildNode(SortedList childNodeNames, string nodeName, object nodeValue)
        {
            // Pre-process contraction of XmlElement-s
            if (nodeValue is XmlElement)
            {
                // Convert  <aa></aa> into "aa":null
                //          <aa>xx</aa> into "aa":"xx"
                var cnode = (XmlNode)nodeValue;

                if (cnode.Attributes.Count == 0)
                {
                    var children = cnode.ChildNodes;

                    if (children.Count == 0)
                    {
                        nodeValue = null;
                    }
                    else if (children.Count == 1 && (children[0] is XmlText))
                    {
                        nodeValue = ((XmlText) (children[0])).InnerText;
                    }
                }
            }
            // Add nodeValue to ArrayList associated with each nodeName
            // If nodeName doesn't exist then add it
            object oValuesAL = childNodeNames[nodeName];
            
            ArrayList ValuesAL;
            
            if (oValuesAL == null)
            {
                ValuesAL = new ArrayList();
                childNodeNames[nodeName] = ValuesAL;
            }
            else
            {
                ValuesAL = (ArrayList) oValuesAL;

            }

            ValuesAL.Add(nodeValue);
        }

        private static void OutputNode(string childname, object alChild, StringBuilder sbJSON, bool showNodeName)
        {
            if (alChild == null)
            {
                if (showNodeName)
                {
                    sbJSON.Append("\"" + SafeJSON(childname) + "\": ");
                }

                sbJSON.Append("null");
            }
            else if (alChild is string)
            {
                if (showNodeName)
                {
                    sbJSON.Append("\"" + SafeJSON(childname) + "\": ");
                }

                var sChild = (string)alChild;

                sChild = sChild.Trim();

                sbJSON.Append("\"" + SafeJSON(sChild) + "\"");
            }
            else
            {
                XmlToJSONnode(sbJSON, (XmlElement) alChild, showNodeName);
            }

            sbJSON.Append(", ");
        }

        // Make a string safe for JSON
        private static string SafeJSON(string sIn)
        {
            var sbOut = new StringBuilder(sIn.Length);
            
            foreach (char ch in sIn)
            {
                if (Char.IsControl(ch) || ch == '\'')
                {
                    var ich = (int)ch;

                    sbOut.Append(@"\u" + ich.ToString("x4"));

                    continue;
                }
                
                if (ch == '\"' || ch == '\\' || ch == '/')
                {
                    sbOut.Append('\\');
                }

                sbOut.Append(ch);
            }
            return sbOut.ToString();
        }
        #endregion
    }
}